﻿angular.module('directives.crud.edit', [])

// Apply this directive to an element at or below a form that will manage CRUD operations on a resource.
// - The resource must expose the following instance methods: $saveOrUpdate(), $id() and $remove()
.directive('crudEdit', ['$parse', function($parse) {
    return {
        // We ask this directive to create a new child scope so that when we add helper methods to the scope
        // it doesn't make a mess of the parent scope.
        // - Be aware that if you write to the scope from within the form then you must remember that there is a child scope at the point
        scope: true,
        // We need access to a form so we require a FormController from this element or a parent element
        require: '^form',
        // This directive can only appear as an attribute
        link: function (scope, element, attrs, form) {
            // We extract the value of the crudEdit attribute
            // - it should be an assignable expression evaluating to the model (resource) that is going to be edited
            var resourceGetter = $parse(attrs.crudEdit);
            var resourceSetter = resourceGetter.assign;
            // Store the resource object for easy access
            var resource = resourceGetter(scope);
            // Store a copy for reverting the changes
            var original = angular.copy(resource);

            var checkResourceMethod = function (methodName) {
                if (!angular.isFunction(resource[methodName])) {
                    throw new Error('crudEdit directive: The resource must expose the ' + methodName + '() instance method');
                }
            };
            checkResourceMethod('$saveOrUpdate');
            checkResourceMethod('$id');
            checkResourceMethod('$remove');

            // This function helps us extract the callback functions from the directive attributes
            var makeFn = function (attrName) {
                var fn = scope.$eval(attrs[attrName]);
                if (!angular.isFunction(fn)) {
                    throw new Error('crudEdit directive: The attribute "' + attrName + '" must evaluate to a function');
                }
                return fn;
            };
            // Set up callbacks with fallback
            // onSave attribute -> onSave scope -> noop
            var userOnSave = attrs.onSave ? makeFn('onSave') : (scope.onSave || angular.noop);
            var onSave = function (result, status, headers, config) {
                // Reset the original to help with reverting and pristine checks
                original = result;
                userOnSave(result, status, headers, config);
            };
            // onRemove attribute -> onRemove scope -> onSave attribute -> onSave scope -> noop
            var onRemove = attrs.onRemove ? makeFn('onRemove') : (scope.onRemove || onSave);
            // onError attribute -> onError scope -> noop
            var onError = attrs.onError ? makeFn('onError') : (scope.onError || angular.noop);

            var onUpdate = attrs.onUpdate ? makeFn('onUpdate') : (scope.onUpdate || angular.noop);

            // The following functions should be triggered by elements on the form
            // - e.g. ng-click="save()"
            scope.save = function () {
                resource.$saveOrUpdate(onSave, onUpdate, onError, onError);
            };
            scope.revertChanges = function () {
                resource = angular.copy(original);
                resourceSetter(scope, resource);
                form.$setPristine();
            };
            scope.remove = function () {
                if (resource.$id()) {
                    resource.$remove(onRemove, onError);
                } else {
                    onRemove();
                }
            };

            // The following functions can be called to modify the behaviour of elements in the form
            // - e.g. ng-disable="!canSave()"
            scope.canSave = function () {
                return form.$valid && !angular.equals(resource, original);
            };
            scope.canRevert = function () {
                return !angular.equals(resource, original);
            };
            scope.canRemove = function () {
                return resource.$id();
            };
            /**
            * Get the CSS classes for this item, to be used by the ng-class directive
            * @param {string} fieldName The name of the field on the form, for which we want to get the CSS classes
            * @return {object} A hash where each key is a CSS class and the corresponding value is true if the class is to be applied.
            */
            scope.getCssClasses = function (fieldName) {
                var ngModelController = form[fieldName];
                return {
                    error: ngModelController.$invalid && !angular.equals(resource, original),
                    success: ngModelController.$valid && !angular.equals(resource, original)
                };
            };
            /**
            * Whether to show an error message for the specified error
            * @param {string} fieldName The name of the field on the form, of which we want to know whether to show the error
            * @param  {string} error - The name of the error as given by a validation directive
            * @return {Boolean} true if the error should be shown
            */
            scope.showError = function (fieldName, error) {
                return form[fieldName].$error[error];
            };
        }
    };
}]);
